Categoría: Observabilidad

  • Dejar que PHP narre su runtime: guía práctica

    Dejar que PHP narre su runtime: guía práctica

    \n

    Resumen: este artículo muestra cómo aplicar un patrón de \”Narrator\” en PHP para que la propia aplicación narre su flujo de ejecución. Incluye un diseño básico, integración y ejemplos de código listos para adaptar.

    \n\n\n\n\n\n\n\n

    Introducción

    \n\n\n\n

    Las soluciones tradicionales de observabilidad entregan señales reactivas: métricas, trazas y logs que requieren correlación externa. En lugar de depender exclusivamente de herramientas externas, podemos hacer que la aplicación misma produzca una narración del runtime: eventos con contexto e intención unidos en hilos causales.

    \n\n\n\n

    Este enfoque complementa (no necesariamente reemplaza) la observabilidad convencional: su objetivo es mejorar la comprensibilidad interna —por ejemplo, por qué se tomó una decisión— y facilitar la depuración rápida sin saltar entre múltiples herramientas.

    \n\n\n\n

    Prerrequisitos

    \n\n\n\n
    • Entorno PHP donde puedas anexar objetos al ciclo de vida de la petición (middleware o front controller).
    • Un logger compatible PSR-3 (por ejemplo Monolog) o un adaptador propio para exportar hilos de eventos cuando sea necesario.
    • Mínima estructura para identificar contexto: request id, session id o user id para correlación.
    \n\n\n\n

    Desarrollo

    \n\n\n\n

    Procedimiento

    \n\n\n\n
    1. Diseñar una clase Narrator que viaje con la petición y acumule eventos con metadatos (tipo, intención, contexto, timestamp).
    2. Instrumentar puntos clave: routing, validaciones, intentos de reintento, decisiones que afecten el flujo.
    3. Decidir retención: caducar segmentos de memoria cuando ya no aporten valor, o agrupar eventos en hilos causales.
    4. Exportar o exponer los hilos en un formato que un visor timeline pueda consumir (JSON, evento estructurado a logger, API interna).
    \n\n\n\n

    A continuación se muestra una implementación mínima de una clase Narrator que acumula eventos y permite serializarlos. Es un punto de partida para adaptar a frameworks o middlewares.

    \n\n\n\n
    <?php\nnamespace App\Observability;\n\nclass Narrator\n{\n    private array $threads = [];\n    private array $currentContext = [];\n\n    public function startThread(string $id, array $meta = []): void\n    {\n        $this->threads[$id] = [\n            'id' => $id,\n            'meta' => $meta,\n            'events' => [],\n        ];\n    }\n\n    public function annotate(string $threadId, string $type, string $message, array $context = []): void\n    {\n        $this->threads[$threadId]['events'][] = [\n            'ts' => microtime(true),\n            'type' => $type,\n            'message' => $message,\n            'context' => $context,\n        ];\n    }\n\n    public function setContext(array $context): void\n    {\n        $this->currentContext = $context;\n    }\n\n    public function expireThread(string $threadId): void\n    {\n        unset($this->threads[$threadId]);\n    }\n\n    public function export(): array\n    {\n        return $this->threads;\n    }\n}\n
    \n\n\n\n

    Integrar el Narrator con el flujo de la petición (por ejemplo como middleware) permite que cada controlador o servicio registre decisiones y motivos, y al final de la petición exporte el hilo a un logger o lo envíe a un endpoint interno.

    \n\n\n\n
    <?php\n// Ejemplo simplificado de middleware (framework-agnóstico)\nuse App\\Observability\\Narrator;\nuse Psr\\Log\\LoggerInterface;\n\nclass NarratorMiddleware\n{\n    private Narrator $narrator;\n    private LoggerInterface $logger;\n\n    public function __construct(Narrator $narrator, LoggerInterface $logger)\n    {\n        $this->narrator = $narrator;\n        $this->logger = $logger;\n    }\n\n    public function handle($request, $next)\n    {\n        $requestId = $request->getAttribute('request_id') ?? uniqid('req_', true);\n        $this->narrator->startThread($requestId, ['path' => $request->getUri()]);\n        $this->narrator->setContext(['request_id' => $requestId]);\n\n        $response = $next($request);\n\n        // Al final de la petición exportamos el hilo\n        $threads = $this->narrator->export();\n        $this->logger->info('narrator.threads', ['threads' => $threads]);\n\n        return $response;\n    }\n}\n
    \n\n\n\n

    En el ejemplo anterior, el logger recibe la estructura completa; un visor de timeline o un consumidor puede mostrar los eventos como una historia por petición. También puede enviarse a un índice o guardarse en almacenamiento temporal.

    \n\n\n\n
    {\n  \"req_606e2a\": {\n    \"id\": \"req_606e2a\",\n    \"meta\": {\"path\": \"/checkout\"},\n    \"events\": [\n      {\"ts\": 1690000000.123, \"type\": \"decision\", \"message\": \"cookie override - route B\", \"context\": {\"cookie\": \"promo\"}},\n      {\"ts\": 1690000000.456, \"type\": \"intent\", \"message\": \"calculate delivery estimate\", \"context\": {\"address\": \"...\"}},\n      {\"ts\": 1690000000.789, \"type\": \"notice\", \"message\": \"missing delivery estimate - user exited\", \"context\": {}}\n    ]\n  }\n}\n
    \n\n\n\n

    Ejemplos

    \n\n\n\n

    Ejemplo de uso dentro de una función de negocio: anotar la intención antes de ejecutar y el resultado después. Así se preserva la causalidad entre intención y efecto.

    \n\n\n\n
    <?php\n// Dentro de un servicio\nfunction applyCoupon(Narrator $narrator, string $threadId, array $couponData)\n{\n    $narrator->annotate($threadId, 'intent', 'apply coupon', ['coupon' => $couponData['code']]);\n\n    // Lógica real\n    $applied = false;\n    // ... comprobar validaciones, límites, fecha ...\n\n    if ($applied) {\n        $narrator->annotate($threadId, 'result', 'coupon applied', ['discount' => 10]);\n    } else {\n        $narrator->annotate($threadId, 'result', 'coupon rejected', ['reason' => 'expired']);\n    }\n\n    return $applied;\n}\n
    \n\n\n\n

    Con esta información, los equipos de producto y soporte pueden leer la cadena de eventos y comprender qué decisión tomó la aplicación y por qué, sin reconstruir el estado a partir de múltiples fuentes.

    \n\n\n\n

    Checklist

    \n\n\n\n
    1. Decidir el alcance de narración: qué tipos de eventos y qué contexto incluir.
    2. Agregar un objeto Narrator accesible en el ciclo de vida de la petición.
    3. Instrumentar puntos críticos: routing, validaciones, retries, fallos y decisiones de negocio.
    4. Definir política de expiración o agregación para evitar ruido innecesario.
    5. Elegir destino de exportación: logger estructurado, índice temporal o UI de timeline.
    6. Validar que los eventos incluyan identificadores para correlación (request id, session id).
    \n\n\n\n

    Conclusión

    \n\n\n\n

    Hacer que PHP narre su propio runtime reduce la necesidad de interpretar trazas desconectadas y acelera la resolución de problemas. Empieza por una implementación pequeña: una clase Narrator, puntos de instrumentación selectos y un canal para exportar hilos.

    \n\n\n\n

    Este patrón no elimina herramientas de observabilidad, pero aporta una capa de contexto y causalidad que hace a las aplicaciones más autoexplicativas y a los equipos menos dependientes de correlaciones externas.

    \n\n
  • Registrar cada llamada API sin ralentizar el servidor

    Registrar cada llamada API sin ralentizar el servidor

    Resumen: Cómo registrar cada llamada a tu API sin degradar el rendimiento. Enfoque práctico: logging asíncrono, buffering, offload a workers, enmascarado de datos y rotación.

    Introducción

    Registrar cada petición API es crítico para depuración, auditoría y análisis, pero hacerlo mal puede convertir el logging en el principal cuello de botella.

    La regla de oro: nunca hagas que el manejador principal espere a que los logs se escriban. El registro debe ser asíncrono y, preferiblemente, salir del camino de ejecución principal.

    Prerrequisitos

    Los ejemplos usan Node.js y bibliotecas comunes; adapta las ideas a otros runtimes si es necesario.

    • Dependencia de un logger de alto rendimiento (p. ej. Pino).
    • Mecanismo para buffering o una cola (memoria, Redis, Kafka).
    • Worker o servicio externo para persistencia y envío a sistemas centralizados.

    Desarrollo

    Procedimiento

    Paso 1 — Decide qué registrar: evita sobre-logging. Campos típicos: timestamp, método, ruta, status, latencia, IP, userId, requestId y payload con datos sensibles enmascarados.

    {
      "time": "2025-08-11T14:23:45.123Z",
      "method": "POST",
      "url": "/api/orders",
      "status": 201,
      "responseTimeMs": 123,
      "userId": "usr_1a2b3c",
      "ip": "203.0.113.42",
      "userAgent": "Mozilla/5.0 (Macintosh...)",
      "requestId": "req_abcd1234"
    }
    Lenguaje del código: JSON / JSON con comentarios (json)

    Paso 2 — Usa un logger no bloqueante. Olvida console.log en producción; emplea un logger diseñado para escritura asíncrona y serialización rápida.

    const pino = require('pino');
    const logger = pino({
      level: 'info',
      transport: {
        target: 'pino-pretty'
      }
    });
    
    app.use((req, res, next) => {
      const start = Date.now();
      
      res.on('finish', () => {
        logger.info({
          method: req.method,
          url: req.originalUrl,
          status: res.statusCode,
          responseTime: Date.now() - start
        });
      });
      
      next();
    });
    Lenguaje del código: JavaScript (javascript)

    Paso 3 — Bufferiza en memoria y envía en lotes. Convierte miles de escrituras pequeñas en pocas escrituras grandes.

    let logBuffer = [];
    const BUFFER_SIZE = 50;
    const FLUSH_INTERVAL = 5000; // ms
    
    function flushLogs() {
      if (logBuffer.length > 0) {
        logger.info({ batch: logBuffer });
        logBuffer = [];
      }
    }
    
    setInterval(flushLogs, FLUSH_INTERVAL);
    
    app.use((req, res, next) => {
      const start = Date.now();
    
      res.on('finish', () => {
        logBuffer.push({
          method: req.method,
          url: req.originalUrl,
          status: res.statusCode,
          time: Date.now(),
          latency: Date.now() - start
        });
    
        if (logBuffer.length >= BUFFER_SIZE) {
          flushLogs();
        }
      });
    
      next();
    });
    Lenguaje del código: JavaScript (javascript)

    Paso 4 — Desacopla mediante cola y workers: para tráfico intenso, envía logs a una cola y procesa desde procesos independientes.

    const Redis = require('ioredis');
    const pub = new Redis();
    
    app.use((req, res, next) => {
      const start = Date.now();
    
      res.on('finish', () => {
        pub.publish('api_logs', JSON.stringify({
          method: req.method,
          url: req.originalUrl,
          status: res.statusCode,
          latency: Date.now() - start
        }));
      });
    
      next();
    });
    Lenguaje del código: JavaScript (javascript)
    const sub = new Redis();
    sub.subscribe('api_logs');
    sub.on('message', (channel, message) => {
      const logData = JSON.parse(message);
      // Persistir logData de forma asíncrona (BD, ELK, S3...)
    });
    Lenguaje del código: JavaScript (javascript)

    Paso 5 — Envío remoto asíncrono: si mandas logs a sistemas como ELK o CloudWatch, hazlo en lotes o por workers para evitar latencia en el request path.

    const winston = require('winston');
    require('winston-cloudwatch');
    
    winston.add(new winston.transports.CloudWatch({
      logGroupName: 'my-api-logs',
      logStreamName: 'production',
      awsRegion: 'us-east-1',
      jsonMessage: true
    }));
    
    app.use((req, res, next) => {
      const start = Date.now();
      res.on('finish', () => {
        winston.info({
          method: req.method,
          url: req.originalUrl,
          status: res.statusCode,
          latency: Date.now() - start
        });
      });
      next();
    });
    Lenguaje del código: JavaScript (javascript)

    Paso 6 — Enmascara datos sensibles antes de loguear: nunca escribas contraseñas, números de tarjeta o tokens sin protección.

    function maskSensitive(obj) {
      const clone = { ...obj };
      if (clone.password) clone.password = '******';
      if (clone.cardNumber) clone.cardNumber = '**** **** **** ' + clone.cardNumber.slice(-4);
      return clone;
    }
    
    app.use(express.json());
    app.use((req, res, next) => {
      req.body = maskSensitive(req.body);
      next();
    });
    Lenguaje del código: JavaScript (javascript)

    Paso 7 — Mide y ajusta: compara latencias y uso de CPU/memoria antes y después del cambio. Usa herramientas de carga y APM para validar impacto.

    autocannon -c 100 -d 30 http://localhost:3000
    Lenguaje del código: Bash (bash)

    Paso 8 — Rotación y archivo: configura rotación por tamaño o día y mueve logs antiguos a almacenamiento económico.

    pino server.js | tee >(pino-pretty) | rotatelogs ./logs/api-%Y-%m-%d.log 86400
    Lenguaje del código: Bash (bash)

    Paso 9 — Distribuye a gran escala: evita escribir localmente, usa shippers (Filebeat/Fluent Bit), Kafka para ingesta masiva y sampling para reducir volumen de success logs.

    Paso 10 — Arquitectura de producción (resumen): gateway → collectors/colas → workers → almacenamiento indexable → dashboards y alertas.

    Ejemplos

    Los ejemplos anteriores cubren patrones comunes: logger rápido (Pino), buffering en proceso, publicación a Redis y consumo por workers, y envío asíncrono a servicios remotos.

    Checklist

    1. No bloquear el request path: logging asíncrono.
    2. Usar un logger de alto rendimiento (p. ej. Pino).
    3. Bufferizar y enviar en lotes.
    4. Offload a workers o colas para tráfico intenso.
    5. Enmascarar datos sensibles antes de persistir.
    6. Rotación y archivado de logs.
    7. Medir impacto y ajustar frecuencias/ tamaños de lote.

    Conclusión

    Registrar cada llamada API es viable si se diseña para no bloquear el flujo principal: emplea loggers rápidos, buffering, colas y workers; enmascara datos; y monitoriza el costo del logging.

    Aplica estas prácticas progresivamente y verifica con pruebas de carga y APM antes de desplegar en producción.